<?php

namespace app\common\service;

use app\libs\exception\ParameterException;

/**
 * Class FileManager
 *
 * https://github.com/prasathmani/tinyfilemanager
 */
class FileManager
{
    protected $rootPath;
    protected $rootUrl;
    protected $path;

    public function __construct($root_path = '', $params = [])
    {
        if (!$root_path) {
            $root_path = base_path();
            if (strstr($root_path, '/public', true)) {
                $root_path = strstr($root_path, '/public', true);
            }
            $root_path = rtrim($root_path, '\\/');
        }
        $path = isset($params['path']) ? urldecode($params['path']) : '';
        if ($path) {
            $root_path .= '/' . $path;
        }
        if (!is_dir($root_path)) {
            throw new ParameterException([
                'code' => 200,
                'msg' => '不是有效文件路径',
            ]);
        }
        $this->rootPath = str_replace('\\', '/', $root_path);

        $this->path = $path;
        // 上级(用于跳转到上一页面)
        //$parent = $this->getParentPath($path);

        $is_https = isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
        $http_host = request()->host();
        $root_url = ($is_https ? 'https' : 'http') . '://' . $http_host;
        $this->rootUrl = $root_url;
    }

    /**
     * 所有文件
     */
    public function scanAllDir($params = [])
    {
        $file_path = $this->rootPath;
        $path = $this->path;

        $objects = is_readable($file_path) ? scandir($file_path) : array();
        $folders = array();
        $files = array();
        $current_path = array_slice(explode("/", $file_path), -1)[0];
        if (is_array($objects)) {
            foreach ($objects as $file) {
                if ($file == '.' || $file == '..') {
                    continue;
                }
                if (substr($file, 0, 1) === '.') {
                    continue;
                }
                $new_path = $file_path . '/' . $file;
                if (@is_file($new_path)) {
                    $files[] = $file;
                } elseif (@is_dir($new_path) && $file != '.' && $file != '..') {
                    $folders[] = $file;
                }
            }
        }

        $num_files = count($files);
        $num_folders = count($folders);
        $all_files_size = 0;
        $folder_infos = [];
        foreach ($folders as $folder) {
            $filename = htmlspecialchars($folder, ENT_QUOTES, 'UTF-8');
            $filename = iconv('UTF-8', 'UTF-8//IGNORE', $filename);
            $icon = 'fa fa-folder-o';
            $modif_raw = filemtime($file_path . '/' . $folder);
            $modif = date('Y-m-d H:i:s', $modif_raw);
            $filesize_raw = '';
            $filesize = '文件夹';
            $filelink = urlencode(trim($path . '/' . $folder, '/'));
            $perms = substr(decoct(fileperms($file_path . '/' . $folder)), -4);
            if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) {
                $owner = posix_getpwuid(fileowner($file_path . '/' . $folder));
                $group = posix_getgrgid(filegroup($file_path . '/' . $folder));
            } else {
                $owner = array('name' => '?');
                $group = array('name' => '?');
            }

            $folder_infos[] = [
                'name' => $filename,
                'perms' => $perms,
                'owner' => $owner,
                'group' => $group,
                'link' => $filelink,
                'icon' => $icon,
                'modif_raw' => $modif_raw,
                'modif' => $modif,
                'size' => str_pad($filesize_raw, 18, '0', STR_PAD_LEFT),
                'filesize' => $filesize
            ];
            flush();
        }

        $file_infos = [];
        foreach ($files as $file) {
            $filename = htmlspecialchars($file, ENT_QUOTES, 'UTF-8');
            $filename = iconv('UTF-8', 'UTF-8//IGNORE', $filename);
            $icon = $this->getFileIconClass($file_path . '/' . $file); // 'fa fa-file-text-o'
            $modif_raw = filemtime($file_path . '/' . $file);
            $modif = date('Y-m-d H:i:s', $modif_raw);
            $filesize_raw = $this->getSize($file_path . '/' . $file);
            $filesize = $this->formatFileBytes($filesize_raw);
            $filelink = '';
            $all_files_size += $filesize_raw;
            $perms = substr(decoct(fileperms($file_path . '/' . $file)), -4);
            if (function_exists('posix_getpwuid') && function_exists('posix_getgrgid')) {
                $owner = posix_getpwuid(fileowner($file_path . '/' . $folder));
                $group = posix_getgrgid(filegroup($file_path . '/' . $folder));
            } else {
                $owner = array('name' => '?');
                $group = array('name' => '?');
            }

            $file_infos[] = [
                'name' => $filename,
                'perms' => $perms,
                'owner' => $owner,
                'group' => $group,
                'link' => $filelink,
                'icon' => $icon,
                'modif_raw' => $modif_raw,
                'modif' => $modif,
                'size' => str_pad($filesize_raw, 18, '0', STR_PAD_LEFT),
                'filesize' => $filesize
            ];
            flush();
        }
        //$location = '<a href="javascript:;" data-path=""><i class="fa fa-home" title="' . $this->rootPath . '"></i></a>';
        //$sep = '<i class="bread-crumb"> / </i>';
        $breadcrumb = [
            ['name' => '', 'path' => '']
        ];
        if ($path != '') {
            $path_arr = explode('/', $path);
            $count = count($path_arr);
            //$location_paths = array();
            $parent = '';
            for ($i = 0; $i < $count; $i++) {
                $parent = trim($parent . '/' . $path_arr[$i], '/');
                $parent_enc = urlencode($parent);
                $title = iconv('UTF-8', 'UTF-8//IGNORE', $path_arr[$i]);
                $title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
                //$location_paths[] = '<a href="javascript:;" data-path="' . $parent_enc . '">' . $title . '</a>';
                $breadcrumb[] = ['name' => $title, 'path' => $parent_enc];
            }
            //$location .= $sep . implode($sep, $location_paths);
        }

        return [
            //'location' => $location,
            'breadcrumb' => $breadcrumb,
            //'files' => $files,
            //'folders' => $folders,
            'folder_infos' => $folder_infos,
            'file_infos' => $file_infos,
            'num_folders' => $num_folders,
            'num_files' => $num_files,
            'all_files_size' => $this->formatFileBytes($all_files_size),
            'partition_size' => $this->formatFileBytes(@disk_total_space($file_path)),
            'free_of_size' => $this->formatFileBytes(@disk_free_space($file_path)),
        ];
    }

    /**
     * 重命名
     */
    public function rename($params)
    {
        // old name
        $old = $params['from_name'];
        $old = $this->cleanPath($old);
        $old = str_replace('/', '', $old);
        // new name
        $new = $params['name'];
        $new = $this->cleanPath(strip_tags($new));
        $new = str_replace('/', '', $new);
        // path
        $file_path = $this->rootPath;
        if (strpbrk($new, '/?%*:|"<>') === false && $old != '' && $new != '') {
            $old = $file_path . '/' . $old;
            $new = $file_path . '/' . $new;
            if (!file_exists($old) || file_exists($new)) {
                return false;
            }
            if (!rename($old, $new)) {
                return false;
            }
        } else {
            return false;
        }
        return true;
    }

    /**
     * 删除
     */
    public function delete($params)
    {
        $file = $params['name'];
        $file = $this->cleanPath(strip_tags($file));
        $file = str_replace('/', '', $file);
        if ($file != '' && $file != '..' && $file != '.') {
            // path
            $file_path = $this->rootPath;
            if (!$this->delFile($file_path . '/' . $file)) {
                return false;
            }
        } else {
            return false;
        }

        return true;
    }

    /**
     * 查看文件
     */
    public function viewer($params)
    {
        $file = $params['name'];
        $file = $this->cleanPath($file, false);
        $file = str_replace('/', '', $file);
        $root_path = $this->rootPath;
        if ($file == '' || !is_file($root_path . '/' . $file)) {
            throw new ParameterException([
                'code' => 200,
                'msg' => '不是有效文件路径',
            ]);
        }

        $file_url = $this->rootUrl . ($this->path != '' ? '/' . $this->path : '') . '/' . $file;
        $file_path = $root_path . '/' . $file;

        $ext = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
        $mime_type = $this->getMimeTtype($file_path);
        $filesize_raw = $this->getSize($file_path);
        $filesize = $this->formatFileBytes($filesize_raw);

        $is_zip = false;
        $is_gzip = false;
        $is_image = false;
        $is_audio = false;
        $is_video = false;
        $is_text = false;
        $view_title = 'File';
        $filenames = false; // for zip
        $content = ''; // for text

        $image_exts = array('ico', 'gif', 'jpg', 'jpeg', 'jpc', 'jp2', 'jpx', 'xbm', 'wbmp', 'png', 'bmp', 'tif', 'tiff', 'psd', 'svg', 'webp', 'avif');
        $video_exts = array('avi', 'webm', 'wmv', 'mp4', 'm4v', 'ogm', 'ogv', 'mov', 'mkv');
        $audio_exts = array('wav', 'mp3', 'ogg', 'm4a');
        $text_exts = array(
            'txt', 'css', 'ini', 'conf', 'log', 'htaccess', 'passwd', 'ftpquota', 'sql', 'js', 'json', 'sh', 'config',
            'php', 'php4', 'php5', 'phps', 'phtml', 'htm', 'html', 'shtml', 'xhtml', 'xml', 'xsl', 'm3u', 'm3u8', 'pls', 'cue',
            'eml', 'msg', 'csv', 'bat', 'twig', 'tpl', 'md', 'gitignore', 'less', 'sass', 'scss', 'c', 'cpp', 'cs', 'py',
            'map', 'lock', 'dtd', 'svg', 'scss', 'asp', 'aspx', 'asx', 'asmx', 'ashx', 'jsx', 'jsp', 'jspx', 'cfm', 'cgi'
        );
        $text_mimes = array(
            'application/xml',
            'application/javascript',
            'application/x-javascript',
            'image/svg+xml',
            'message/rfc822',
        );
        if ($ext == 'zip' || $ext == 'tar') {
            $is_zip = true;
            $view_title = 'Archive';
            $filenames = $this->getZifInfo($file_path, $ext);
        } elseif (in_array($ext, $image_exts)) {
            $is_image = true;
            $view_title = 'Image';
        } elseif (in_array($ext, $video_exts)) {
            $is_audio = true;
            $view_title = 'Audio';
        } elseif (in_array($ext, $audio_exts)) {
            $is_video = true;
            $view_title = 'Video';
        } elseif (in_array($ext, $text_exts) || substr($mime_type, 0, 4) == 'text' || in_array($mime_type, $text_mimes)) {
            $is_text = true;
            $content = file_get_contents($file_path);
        }

        if ($is_zip) {
            // ZIP content
            if ($filenames !== false) {
                $content .= '<code class="maxheight">';
                foreach ($filenames as $fn) {
                    if ($fn['folder']) {
                        $content .= '<b>' . htmlspecialchars($fn['name'], ENT_QUOTES, 'UTF-8') . '</b><br>';
                    } else {
                        $content .= $fn['name'] . ' (' . $this->formatFileBytes($fn['filesize']) . ')<br>';
                    }
                }
                $content .= '</code>';
            }
        } elseif ($is_image) {
            // Image content
            if (in_array($ext, array('gif', 'jpg', 'jpeg', 'png', 'bmp', 'ico', 'svg', 'webp', 'avif'))) {
                $content .= '<p><img src="' . htmlspecialchars($file_url, ENT_QUOTES, 'UTF-8') . '" alt="" class="preview-img"></p>';
            }
        } elseif ($is_audio) {
            // Audio content
            $content .= '<p><audio src="' . htmlspecialchars($file_url, ENT_QUOTES, 'UTF-8') . '" controls preload="metadata"></audio></p>';
        } elseif ($is_video) {
            // Video content
            $content .= '<div class="preview-video"><video src="' . htmlspecialchars($file_url, ENT_QUOTES, 'UTF-8') . '" width="640" height="360" controls preload="metadata"></video></div>';
        } elseif ($is_text) {
            if (in_array($ext, array('php', 'php4', 'php5', 'phtml', 'phps'))) {
                // php highlight
                //$content = highlight_string($content, true);
            }

            //$content = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
            /*// highlight
            $hljs_classes = array(
                'shtml' => 'xml',
                'htaccess' => 'apache',
                'phtml' => 'php',
                'lock' => 'json',
                'svg' => 'xml',
            );
            $text_names = array(
                'license',
                'readme',
                'authors',
                'contributors',
                'changelog',
            );
            $hljs_class = isset($hljs_classes[$ext]) ? 'lang-' . $hljs_classes[$ext] : 'lang-' . $ext;
            if (empty($ext) || in_array(strtolower($file), $text_names) || preg_match('#\.min\.(css|js)$#i', $file)) {
                $hljs_class = 'nohighlight';
            }
            $content = '<pre class="with-hljs"><code class="' . $hljs_class . '">' . htmlspecialchars($content, ENT_QUOTES, 'UTF-8') . '</code></pre>';*/
        }

        return $content;
    }

    /**
     * 防止路径遍历并清除url
     */
    protected function absolutePath($path)
    {
        $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path);
        $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen');
        $absolutes = array();
        foreach ($parts as $part) {
            if ('.' == $part) {
                continue;
            }
            if ('..' == $part) {
                array_pop($absolutes);
            } else {
                $absolutes[] = $part;
            }
        }
        return implode(DIRECTORY_SEPARATOR, $absolutes);
    }

    /**
     * 清除路径
     */
    protected function cleanPath($path, $trim = true)
    {
        $path = $trim ? trim($path) : $path;
        $path = trim($path, '\\/');
        $path = str_replace(array('../', '..\\'), '', $path);
        $path = $this->absolutePath($path);
        if ($path == '..') {
            $path = '';
        }
        return str_replace('\\', '/', $path);
    }

    /**
     * 获取父路径
     */
    protected function getParentPath($path)
    {
        $path = $this->cleanPath($path);
        if ($path != '') {
            $array = explode('/', $path);
            if (count($array) > 1) {
                $array = array_slice($array, 0, -1);
                return implode('/', $array);
            }
            return '';
        }
        return false;
    }

    /**
     * 获取文件大小
     */
    protected function getSize($file)
    {
        static $iswin;
        static $isdarwin;
        if (!isset($iswin)) {
            $iswin = (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN');
        }
        if (!isset($isdarwin)) {
            $isdarwin = (strtoupper(substr(PHP_OS, 0)) == "DARWIN");
        }

        static $exec_works;
        if (!isset($exec_works)) {
            $exec_works = (function_exists('exec') && !ini_get('safe_mode') && @exec('echo EXEC') == 'EXEC');
        }

        // try a shell command
        if ($exec_works) {
            $arg = escapeshellarg($file);
            $cmd = ($iswin) ? "for %F in (\"$file\") do @echo %~zF" : ($isdarwin ? "stat -f%z $arg" : "stat -c%s $arg");
            @exec($cmd, $output);
            if (is_array($output) && ctype_digit($size = trim(implode("\n", $output)))) {
                return $size;
            }
        }

        // try the Windows COM interface
        if ($iswin && class_exists("COM")) {
            try {
                $fsobj = new COM('Scripting.FileSystemObject');
                $f = $fsobj->GetFile(realpath($file));
                $size = $f->Size;
            } catch (Exception $e) {
                $size = null;
            }
            if (ctype_digit($size)) {
                return $size;
            }
        }

        return filesize($file);
    }

    /**
     * 格式化文件字节大小
     */
    protected function formatFileBytes($size)
    {
        $size = (float)$size;
        $units = array('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB');
        $power = $size > 0 ? floor(log($size, 1024)) : 0;
        return sprintf('%s %s', round($size / pow(1024, $power), 2), $units[$power]);
    }

    /**
     * Get mime type
     */
    protected function getMimeTtype($file_path)
    {
        if (function_exists('finfo_open')) {
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $mime = finfo_file($finfo, $file_path);
            finfo_close($finfo);
            return $mime;
        } elseif (function_exists('mime_content_type')) {
            return mime_content_type($file_path);
        } elseif (!stristr(ini_get('disable_functions'), 'shell_exec')) {
            $file = escapeshellarg($file_path);
            $mime = shell_exec('file -bi ' . $file);
            return $mime;
        } else {
            return '--';
        }
    }

    /**
     * Get info about zip archive
     */
    protected function getZifInfo($path, $ext)
    {
        if ($ext == 'zip' && function_exists('zip_open')) {
            $arch = zip_open($path);
            if ($arch) {
                $filenames = array();
                while ($zip_entry = zip_read($arch)) {
                    $zip_name = zip_entry_name($zip_entry);
                    $zip_folder = substr($zip_name, -1) == '/';
                    $filenames[] = array(
                        'name' => $zip_name,
                        'filesize' => zip_entry_filesize($zip_entry),
                        'compressed_size' => zip_entry_compressedsize($zip_entry),
                        'folder' => $zip_folder
                        //'compression_method' => zip_entry_compressionmethod($zip_entry),
                    );
                }
                zip_close($arch);
                return $filenames;
            }
        } elseif ($ext == 'tar' && class_exists('PharData')) {
            $archive = new \PharData($path);
            $filenames = array();
            foreach (new \RecursiveIteratorIterator($archive) as $file) {
                $parent_info = $file->getPathInfo();
                $zip_name = str_replace("phar://" . $path, '', $file->getPathName());
                $zip_name = substr($zip_name, ($pos = strpos($zip_name, '/')) !== false ? $pos + 1 : 0);
                $zip_folder = $parent_info->getFileName();
                $zip_info = new \SplFileInfo($file);
                $filenames[] = array(
                    'name' => $zip_name,
                    'filesize' => $zip_info->getSize(),
                    'compressed_size' => $file->getCompressedSize(),
                    'folder' => $zip_folder
                );
            }
            return $filenames;
        }
        return false;
    }

    /**
     * Get CSS classname for file
     */
    protected function getFileIconClass($path)
    {
        // get extension
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));

        switch ($ext) {
            case 'ico':
            case 'gif':
            case 'jpg':
            case 'jpeg':
            case 'jpc':
            case 'jp2':
            case 'jpx':
            case 'xbm':
            case 'wbmp':
            case 'png':
            case 'bmp':
            case 'tif':
            case 'tiff':
            case 'webp':
            case 'avif':
            case 'svg':
                $icon = 'fa fa-picture-o';
                break;
            case 'passwd':
            case 'ftpquota':
            case 'sql':
            case 'js':
            case 'json':
            case 'sh':
            case 'config':
            case 'twig':
            case 'tpl':
            case 'md':
            case 'gitignore':
            case 'c':
            case 'cpp':
            case 'cs':
            case 'py':
            case 'rs':
            case 'map':
            case 'lock':
            case 'dtd':
                $icon = 'fa fa-file-code-o';
                break;
            case 'txt':
            case 'ini':
            case 'conf':
            case 'log':
            case 'htaccess':
                $icon = 'fa fa-file-text-o';
                break;
            case 'css':
            case 'less':
            case 'sass':
            case 'scss':
                $icon = 'fa fa-css3';
                break;
            case 'bz2':
            case 'zip':
            case 'rar':
            case 'gz':
            case 'tar':
            case '7z':
            case 'xz':
                $icon = 'fa fa-file-archive-o';
                break;
            case 'php':
            case 'php4':
            case 'php5':
            case 'phps':
            case 'phtml':
                $icon = 'fa fa-code';
                break;
            case 'htm':
            case 'html':
            case 'shtml':
            case 'xhtml':
                $icon = 'fa fa-html5';
                break;
            case 'xml':
            case 'xsl':
                $icon = 'fa fa-file-excel-o';
                break;
            case 'wav':
            case 'mp3':
            case 'mp2':
            case 'm4a':
            case 'aac':
            case 'ogg':
            case 'oga':
            case 'wma':
            case 'mka':
            case 'flac':
            case 'ac3':
            case 'tds':
                $icon = 'fa fa-music';
                break;
            case 'm3u':
            case 'm3u8':
            case 'pls':
            case 'cue':
            case 'xspf':
                $icon = 'fa fa-headphones';
                break;
            case 'avi':
            case 'mpg':
            case 'mpeg':
            case 'mp4':
            case 'm4v':
            case 'flv':
            case 'f4v':
            case 'ogm':
            case 'ogv':
            case 'mov':
            case 'mkv':
            case '3gp':
            case 'asf':
            case 'wmv':
                $icon = 'fa fa-file-video-o';
                break;
            case 'eml':
            case 'msg':
                $icon = 'fa fa-envelope-o';
                break;
            case 'xls':
            case 'xlsx':
            case 'ods':
                $icon = 'fa fa-file-excel-o';
                break;
            case 'csv':
                $icon = 'fa fa-file-text-o';
                break;
            case 'bak':
            case 'swp':
                $icon = 'fa fa-clipboard';
                break;
            case 'doc':
            case 'docx':
            case 'odt':
                $icon = 'fa fa-file-word-o';
                break;
            case 'ppt':
            case 'pptx':
                $icon = 'fa fa-file-powerpoint-o';
                break;
            case 'ttf':
            case 'ttc':
            case 'otf':
            case 'woff':
            case 'woff2':
            case 'eot':
            case 'fon':
                $icon = 'fa fa-font';
                break;
            case 'pdf':
                $icon = 'fa fa-file-pdf-o';
                break;
            case 'psd':
            case 'ai':
            case 'eps':
            case 'fla':
            case 'swf':
                $icon = 'fa fa-file-image-o';
                break;
            case 'exe':
            case 'msi':
                $icon = 'fa fa-file-o';
                break;
            case 'bat':
                $icon = 'fa fa-terminal';
                break;
            default:
                $icon = 'fa fa-info-circle';
                break;
        }

        return $icon;
    }

    /**
     * 递归删除文件
     */
    protected function delFile($path)
    {
        if (is_link($path)) {
            return unlink($path);
        } elseif (is_dir($path)) {
            $objects = scandir($path);
            $ok = true;
            if (is_array($objects)) {
                foreach ($objects as $file) {
                    if ($file != '.' && $file != '..') {
                        if (!$this->delFile($path . '/' . $file)) {
                            $ok = false;
                        }
                    }
                }
            }
            return ($ok) ? rmdir($path) : false;
        } elseif (is_file($path)) {
            return unlink($path);
        }
        return false;
    }
}
